package com.jwater.gateway.service;

import com.jwater.core.manager.common.MsgKeys;
import com.jwater.core.manager.common.model.GatewayProxyInfo;
import com.jwater.core.manager.common.model.WorkerInfo;
import com.jwater.core.manager.common.service.JwaterService;
import com.jwater.core.manager.worker.WorkerFactory;
import com.jwater.core.manager.worker.WorkerHandler;
import com.jwater.core.net.Node;
import com.jwater.gateway.model.DestUrlInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description:
 * @author: jnner
 * @createDate: 2021/3/27
 * @version: 1.0
 */
@Service
public class CacheService implements CommandLineRunner, WorkerHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(CacheService.class);

    private Map<String, Map<String, DestUrlInfo>> mappingCache = new ConcurrentHashMap<>();
    private Map<String, Map<String, DestUrlInfo>> wsMappingCache = new ConcurrentHashMap<>();
    private Node node;
    private JwaterService jwaterService;
    private WorkerInfo workerInfo;

    @Override
    public void run(String... args) throws Exception {
        LOGGER.info("run......");
        workerInfo = WorkerFactory.getContext().getWorkerInfo();
        if (workerInfo == null) {
            return;
        }
        WorkerFactory.getContext().setHandler(this);
        node = WorkerFactory.getContext().getNode();
        while (!node.isConnect()) {
            Thread.sleep(10);
        }
        jwaterService = node.getProxy(JwaterService.class);
        updateCache();
        node.syncCall(MsgKeys.ACCEPT_NOTIFY, new byte[0]);
    }

    public void addMapping(String prefix, String url) {
        Map map = getDestUrlInfos(prefix);
        DestUrlInfo destUrlInfo = new DestUrlInfo();
        destUrlInfo.setUrl(url);
        map.put(url, destUrlInfo);
    }

    private Map getDestUrlInfos(String prefix) {
        Map destUrlInfos = mappingCache.get(prefix);
        if (destUrlInfos == null) {
            destUrlInfos = new ConcurrentHashMap();
            mappingCache.put(prefix, destUrlInfos);
        }
        return destUrlInfos;
    }

    public String getMappingByPrefix(String path) {
        for (Map.Entry<String, Map<String, DestUrlInfo>> en : mappingCache.entrySet()) {
            if (path.startsWith(en.getKey())) {
                String destUrl = randomChoose(en.getValue());
                if (destUrl == null) {
                    return null;
                }
                destUrl = destUrl + path.replaceFirst(en.getKey(), "");
                return destUrl;
            }
        }
        return null;
    }

    private Map getWsDestUrlInfos(String prefix) {
        Map destUrlInfos = wsMappingCache.get(prefix);
        if (destUrlInfos == null) {
            destUrlInfos = new ConcurrentHashMap();
            wsMappingCache.put(prefix, destUrlInfos);
        }
        return destUrlInfos;
    }

    public String getWsMappingByPrefix(String path) {
        for (Map.Entry<String, Map<String, DestUrlInfo>> en : wsMappingCache.entrySet()) {
            if (path.startsWith(en.getKey())) {
                String destUrl = randomChoose(en.getValue());
                if (destUrl == null) {
                    return null;
                }
                destUrl = destUrl + path.replaceFirst(en.getKey(), "");
                return destUrl;
            }
        }
        return null;
    }

    private String randomChoose(Map<String, DestUrlInfo> destUrlInfos) {
        if (destUrlInfos.size() == 0) {
            return null;
        }
        int i = new Random(System.currentTimeMillis()).nextInt(destUrlInfos.size());
        int index = 0;
        for (String url : destUrlInfos.keySet()) {
            if (index == i) {
                return url;
            }
            index++;
        }
        return null;
    }

    private void updateCache() {
        List<GatewayProxyInfo> proxyInfos = jwaterService.getGatewayProxyInfo(workerInfo.getGroupName(),
                workerInfo.getWorkerName());
        //标记删除
        markRemove(mappingCache);
        markRemove(wsMappingCache);
        //添加服务
        for (GatewayProxyInfo proxyInfo : proxyInfos) {
            if ("http".equals(proxyInfo.getType())) {
                Map<String, DestUrlInfo> destUrlInfos = getDestUrlInfos(proxyInfo.getSrcPrefix());
                DestUrlInfo destUrlInfo = getDestUrlInfo(destUrlInfos, proxyInfo.getDestUrl());
                if (destUrlInfo != null) {
                    destUrlInfos.put(destUrlInfo.getUrl(), destUrlInfo);
                }
            } else if ("ws".equals(proxyInfo.getType())) {
                Map<String, DestUrlInfo> destUrlInfos = getWsDestUrlInfos(proxyInfo.getSrcPrefix());
                DestUrlInfo destUrlInfo = getDestUrlInfo(destUrlInfos, proxyInfo.getDestUrl());
                if (destUrlInfo != null) {
                    destUrlInfos.put(destUrlInfo.getUrl(), destUrlInfo);
                }
            }
        }
        //删除服务
        removeAll(mappingCache);
        removeAll(wsMappingCache);
        //缓存数据打印
        printCache();
    }

    private void printCache() {
        LOGGER.info("http proxy......");
        for (Map.Entry<String, Map<String, DestUrlInfo>> en : mappingCache.entrySet()) {
            LOGGER.info(en.getKey() + "=>" + en.getValue().keySet());
        }
        LOGGER.info("ws proxy......");
        for (Map.Entry<String, Map<String, DestUrlInfo>> en : wsMappingCache.entrySet()) {
            LOGGER.info(en.getKey() + "=>" + en.getValue().keySet());
        }
    }

    private DestUrlInfo getDestUrlInfo(Map<String, DestUrlInfo> destUrlInfos, String url) {
        DestUrlInfo destUrlInfo = destUrlInfos.get(url);
        if (destUrlInfo != null) {
            destUrlInfo.setRemove(false);
            return null;
        }
        destUrlInfo = new DestUrlInfo();
        destUrlInfo.setUrl(url);
        return destUrlInfo;
    }

    private void markRemove(Map<String, Map<String, DestUrlInfo>> mappingCache) {
        for (Map.Entry<String, Map<String, DestUrlInfo>> en : mappingCache.entrySet()) {
            for (DestUrlInfo destUrlInfo : en.getValue().values()) {
                destUrlInfo.setRemove(true);
            }
        }
    }

    private void removeAll(Map<String, Map<String, DestUrlInfo>> mappingCache) {
        for (Map.Entry<String, Map<String, DestUrlInfo>> en : mappingCache.entrySet()) {
            Map<String, DestUrlInfo> destUrlInfos = remove(en.getValue());
            if (destUrlInfos.isEmpty()) {
                mappingCache.remove(en.getKey());
            }
        }
    }

    private Map<String, DestUrlInfo> remove(Map<String, DestUrlInfo> destUrlInfos) {
        Iterator<DestUrlInfo> it = destUrlInfos.values().iterator();
        while (it.hasNext()) {
            DestUrlInfo destUrlInfo = it.next();
            if (destUrlInfo.isRemove()) {
                destUrlInfos.remove(destUrlInfo.getUrl());
            }
        }
        return destUrlInfos;
    }

    @Override
    public void onExit() {

    }

    @Override
    public void onConnect(Node node) {

    }

    @Override
    public void onDisconnect(Node node) {

    }

    @Override
    public void onData(Node node, String dataType, byte[] data) {
        if (MsgKeys.NOTIFY_WORKER_CHANGE.equals(dataType)) {
            updateCache();
        }
    }

    @Override
    public byte[] onSyncCall(Node node, String method, byte[] args) {
        return new byte[0];
    }

    @Override
    public void onAsyncCall(Node node, String method, byte[] args, ReturnCallback callback) {

    }
}
